﻿using System.Collections.Generic;
using GammaJul.ReSharper.GammaXaml.Highlightings.Xaml;
using GammaJul.ReSharper.GammaXaml.Psi;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using JetBrains.Annotations;
using JetBrains.ReSharper.Daemon.Stages;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;
using JetBrains.ReSharper.Psi.Xaml.Impl.Tree;
using JetBrains.ReSharper.Psi.Xaml.Tree;
using JetBrains.Util;

namespace GammaJul.ReSharper.GammaXaml.Analysis.Xaml {

	/// <summary>
	/// An analyzer that checks redundants nodes:
	///  - Attached setters that have a meaning only on certain types children (AttachedPropertyBrowsableForChildrenAttribute)
	///  - Attached setters that have a meaning only on certain types (AttachedPropertyBrowsableForTypeAttribute)
	///  - Empty collection setters
	/// </summary>
	/// <remarks>Redundant defaults for RowDefinitions/ColumnDefinitions is checked in <see cref="GridDefinitionAnalysis"/>.</remarks>
	public class RedundantNodeAnalysis : XamlAnalysis {
		private readonly Dictionary<IMethod, AttachedDpPlacement> _attachedDpPlacementsBySetter = new Dictionary<IMethod, AttachedDpPlacement>();
		private IXamlTypeCache _xamlTypeCache;
		private IDeclaredType _enumerableType;

		private static readonly ClrTypeName _forChildrenAttrClrName = new ClrTypeName("System.Windows.AttachedPropertyBrowsableForChildrenAttribute");
		private static readonly ClrTypeName _forTypeAttrClrName = new ClrTypeName("System.Windows.AttachedPropertyBrowsableForTypeAttribute");
		private static readonly ClrTypeName _whenAttributePresentAttrClrName = new ClrTypeName("System.Windows.AttachedPropertyBrowsableWhenAttributePresentAttribute");

		private enum AttachedDpChildrenMode {
			None = 0,
			Children,
			Descendants
		}

		/// <summary>
		/// Represents the possible placements of an attached dependency property.
		/// </summary>
		private struct AttachedDpPlacement {

			/// <summary>
			/// Gets the children mode from AttachedPropertyBrowsableForChildrenAttribute.
			/// </summary>
			internal readonly AttachedDpChildrenMode ChildrenMode;

			/// <summary>
			/// Gets the types from AttachedPropertyBrowsableForTypeAttribute.
			/// </summary>
			[CanBeNull]
			internal readonly IList<ITypeElement> OnTypes;

			/// <summary>
			/// Gets the attribute type from AttachedPropertyBrowsableWhenAttributePresentAttribute.
			/// </summary>
			[CanBeNull]
			internal readonly IClrTypeName AttributePresentClrName;

			/// <summary>
			/// Gets whether the dependency property can be placed anywhere.
			/// </summary>
			internal bool IsAnywhere {
				get {
					return ChildrenMode == AttachedDpChildrenMode.None
						&& (OnTypes == null || OnTypes.Count == 0)
						&& AttributePresentClrName == null;
				}
			}

			internal AttachedDpPlacement(AttachedDpChildrenMode childrenMode, [CanBeNull] IList<ITypeElement> onTypes, [CanBeNull] IClrTypeName attributePresentClrName) {
				ChildrenMode = childrenMode;
				OnTypes = onTypes;
				AttributePresentClrName = attributePresentClrName;
			}
		}

		public override void ProcessBeforeInterior(ITreeNode element, IHighlightingConsumer highlightings) {
			var propSetter = element as IPropertySetter;
			if (propSetter == null)
				return;

			ITypeMemberReference memberRef = propSetter.MemberReference;
			if (memberRef == null)
				return;

			IDeclaredElement member = memberRef.Resolve().DeclaredElement;
			if (member == null)
				return;

			CheckRedundant(propSetter as IXamlPropertyElement, member, highlightings);

			var attachedSetter = member as IMethod;
			if (attachedSetter != null)
				CheckMeaningless(propSetter, attachedSetter, highlightings);
		}

		/// <summary>
		/// Checks whether a property element is redundant because it's an empty collection setter.
		/// Adds an highlighting if it's the case.
		/// </summary>
		/// <param name="propElement">The property element to check.</param>
		/// <param name="setterMember">The setter property represented by <paramref name="propElement"/>.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void CheckRedundant([CanBeNull] IXamlPropertyElement propElement, [NotNull] IDeclaredElement setterMember, [NotNull] IHighlightingConsumer highlightings) {
			if (propElement == null)
				return;
			
			if (_enumerableType == null)
				_enumerableType = XamlFile.GetPsiModule().GetPredefinedType().IEnumerable;

			if (!setterMember.Type().IsSubtypeOf(_enumerableType)
			|| propElement.InnerTags.Count > 0
			|| propElement.ContainsErrorElement())
				return;

			var highlighting = new RedundantEmptyCollectionSetterHighlighting(propElement);
			highlightings.AddHighlighting(highlighting, XamlFile);
		}

		/// <summary>
		/// Checks whether a property setter is meaningless because it has to be placed on an element having a specific parent type.
		/// Adds an highlighting if it's the case.
		/// </summary>
		/// <param name="propSetter">The property setter to check</param>
		/// <param name="attachedSetter">The attached setter method represented by <paramref name="attachedSetter"/>.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void CheckMeaningless([NotNull] IPropertySetter propSetter, [NotNull] IMethod attachedSetter, [NotNull] IHighlightingConsumer highlightings) {
			
			// TODO: use a PSI module cache rather than caching by analysis
			AttachedDpPlacement placement;
			if (!_attachedDpPlacementsBySetter.TryGetValue(attachedSetter, out placement)) {
				placement = ComputeDpPlacement(attachedSetter);
				_attachedDpPlacementsBySetter[attachedSetter] = placement;
			}

			if (placement.IsAnywhere)
				return;

			// finds the object where the setter is placed
			var xamlObject = propSetter.GetContainingNode<IXamlObjectElement>();
			if (xamlObject == null || propSetter.ContainsErrorElement())
				return;

			// verify AttachedPropertyBrowsableForChildrenAttribute
			if (placement.ChildrenMode != AttachedDpChildrenMode.None) {
				bool isForDescendants = placement.ChildrenMode == AttachedDpChildrenMode.Descendants;
				CheckMeaninglessForChildren(propSetter, attachedSetter, xamlObject, isForDescendants, highlightings);
			}

			// verify AttachedPropertyBrowsableForTypeAttribute
			if (placement.OnTypes != null && placement.OnTypes.Count > 0)
				CheckMeaninglessForType(propSetter, attachedSetter, xamlObject, placement.OnTypes, highlightings);

			// verify AttachedPropertyBrowsableWhenAttributePresentAttribute
			if (placement.AttributePresentClrName != null)
				CheckMeaninglessWhenAttributePresent(propSetter, attachedSetter, xamlObject, placement.AttributePresentClrName, highlightings);
		}

		/// <summary>
		/// Checks whether a property setter is meaningless because it has to be placed on an element having a specific parent type.
		/// Adds an highlighting if it's the case.
		/// </summary>
		/// <param name="propSetter">The property setter to check</param>
		/// <param name="attachedSetter">The attached setter method represented by <paramref name="attachedSetter"/>.</param>
		/// <param name="owner">The object element owning <paramref name="propSetter"/>.</param>
		/// <param name="isForDescendants">Whether the check is for all descendants (<c>true</c>) or direct children only (<c>false</c>).</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		private void CheckMeaninglessForChildren([NotNull] IPropertySetter propSetter, [NotNull] IMethod attachedSetter,
			[NotNull] ITreeNode owner, bool isForDescendants, [NotNull] IHighlightingConsumer highlightings) {
			
			// finds the object parent
			var xamlObject = owner.GetContainingNode<IXamlObjectElement>();

			ITypeElement expectedContaingType = attachedSetter.GetContainingType();
			Assertion.AssertNotNull(expectedContaingType, "expectedContaingType != null");

			while (xamlObject != null) {

				// ok, we have a parent of the expected type
				ITypeElement objectType = xamlObject.TypeReference.ResolvedType();
				if (objectType != null && objectType.IsDescendantOf(expectedContaingType))
					return;

				// we only search the nearest parent in case of a Children mode
				if (!isForDescendants)
					break;

				// otherwise we're in Descendants mode so we'll walk the tree up as much as we can
				xamlObject = xamlObject.GetContainingNode<IXamlObjectElement>();
			}

			var highlighting = new MeaninglessAttachedSetterForChildrenHighlighting(propSetter, attachedSetter, expectedContaingType, isForDescendants);
			highlightings.AddHighlighting(highlighting, XamlFile);
		}

		/// <summary>
		/// Checks whether a property setter is meaningless because it has to be placed on an element having a specific type.
		/// Adds an highlighting if it's the case.
		/// </summary>
		/// <param name="propSetter">The property setter to check</param>
		/// <param name="attachedSetter">The attached setter method represented by <paramref name="attachedSetter"/>.</param>
		/// <param name="expectedTypes">The expected types of <paramref name="owner"/>.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		/// <param name="owner">The object element owning <paramref name="propSetter"/>.</param>
		private void CheckMeaninglessForType([NotNull] IPropertySetter propSetter, [NotNull] IMethod attachedSetter,
			[NotNull] ITypeUsage owner, [NotNull] IList<ITypeElement> expectedTypes, [NotNull] IHighlightingConsumer highlightings) {

			ITypeElement xamlObjectType = owner.TypeReference.ResolvedType();
			if (xamlObjectType == null)
				return;

			foreach (ITypeElement expectedType in expectedTypes) {
				if (xamlObjectType.IsDescendantOf(expectedType))
					return;
			}

			var highlighting = new MeaninglessAttachedSetterForTypeHighlighting(propSetter, attachedSetter, expectedTypes);
			highlightings.AddHighlighting(highlighting, XamlFile);
		}

		/// <summary>
		/// Checks whether a property setter is meaningless because it has to be placed on an element having a specific type.
		/// Adds an highlighting if it's the case.
		/// </summary>
		/// <param name="propSetter">The property setter to check</param>
		/// <param name="attachedSetter">The attached setter method represented by <paramref name="attachedSetter"/>.</param>
		/// <param name="attributePresentClrName">The attribute type name that must be present.</param>
		/// <param name="highlightings">The highlighting consumer to use to add highlightings.</param>
		/// <param name="owner">The object element owning <paramref name="propSetter"/>.</param>
		private void CheckMeaninglessWhenAttributePresent([NotNull] IPropertySetter propSetter, [NotNull] IMethod attachedSetter,
			[NotNull] ITypeUsage owner, [NotNull] IClrTypeName attributePresentClrName, [NotNull] IHighlightingConsumer highlightings) {

			ITypeElement xamlObjectType = owner.TypeReference.ResolvedType();
			if (xamlObjectType == null
			|| xamlObjectType.HasAttributeInstance(attributePresentClrName, true))
				return;

			var highlighting = new MeaninglessAttachedSetterWhenAttributePresentHighlighting(propSetter, attachedSetter, attributePresentClrName);
			highlightings.AddHighlighting(highlighting, XamlFile);
		}

		/// <summary>
		/// Computes whether an attached setter has a meaning only on certain elements.
		/// This is done by searching for AttachedPropertyBrowsableAttribute attributes.
		/// </summary>
		/// <param name="attachedSetter">The dependency property attached setter.</param>
		/// <returns>A <see cref="AttachedDpPlacement"/></returns>
		private AttachedDpPlacement ComputeDpPlacement([NotNull] IMethod attachedSetter) {
			if (_xamlTypeCache == null)
				_xamlTypeCache = XamlTypeCache.GetCache(XamlFile.GetPsiModule());

			if (!attachedSetter.IsDpAttachedSetter(_xamlTypeCache))
				return new AttachedDpPlacement();

			IMethod[] getters = attachedSetter.GetContainingType().GetDpAttachedGetters(attachedSetter.GetExpectedDpName());
			var childrenMode = AttachedDpChildrenMode.None;
			var types = new LocalList<ITypeElement>();
			IClrTypeName attributePresentClrName = null;

			foreach (IMethod getter in getters) {
				foreach (IAttributeInstance attribute in getter.GetAttributeInstances(false)) {
					IClrTypeName attrClrName = attribute.GetClrName();

					// AttachedPropertyBrowsableForChildrenAttribute
					if (attrClrName.Equals(_forChildrenAttrClrName)) {
						AttributeValue v = attribute.NamedParameter("IncludeDescendants");
						childrenMode = !v.IsBadValue && v.IsConstant && v.ConstantValue.IsBoolean() && (bool) v.ConstantValue.Value
							? AttachedDpChildrenMode.Descendants
							: AttachedDpChildrenMode.Children;
					}

					// AttachedPropertyBrowsableForTypeAttribute
					if (attrClrName.Equals(_forTypeAttrClrName)) {
						AttributeValue v = attribute.PositionParameter(0);
						if (v.IsType)
							types.Add(v.TypeValue.GetTypeElement<ITypeElement>());
					}

					// AttachedPropertyBrowsableWhenAttributePresentAttribute
					if (attrClrName.Equals(_whenAttributePresentAttrClrName)) {
						AttributeValue v = attribute.PositionParameter(0);
						if (v.IsType) {
							var typeElement = v.TypeValue.GetTypeElement<ITypeElement>();
							if (typeElement != null)
								attributePresentClrName = typeElement.GetClrName();
						}
					}

				}
			}
			return new AttachedDpPlacement(childrenMode, types.ResultingList(), attributePresentClrName);
		}

	}

}